﻿using Sharp7;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
using Zhaoxi.S7Lib;

namespace Zhaoxi.SiemensS7
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello, World!");

            

            // S7协议报文测试
            {
                //S7Connect();

                //S7Read();

                //S7Write();

                //S7Run();

                //S7Stop();

                //S7GetDateTime();

                //S7SetDateTime();
            }

            Console.ReadLine();
        }

        

        #region S7socket通信过程演示
        /// <summary>
        /// socket对象 - TCP通信
        /// </summary>
        static Socket socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
        /// <summary>
        /// S7协议连接PLC过程 - 三次握手
        /// 实际项目中建议使用第三方库完成通信
        /// </summary>
        static void S7Connect()
        {
            #region 第一次握手
            // 1、TCP三次握手，建立TCP通信   Socket/TcpClient
            socket.Connect("192.168.1.100", 102);
            #endregion

            #region 第二次握手
            // 2、COTP
            byte[] bytes = new byte[] {
                // TPKT
                0x03, // 版本号默认3 - 固定值
                0x00, // 保留字节 - 固定0
                0x00,0x16, // 整个请求字节数

                // COTP
                0x11, // 在COTP这个环节，当前字节以后的字节数
                0xe0, // 表示连接请求
                0x00,0x00, // 协议类 - 默认值
                0x00,0x01, // 目的TSAP的引用 - PLC的连接端口号 - 默认值
                0x00, // 源TSAP的引用 - PC的连接端口号 - 默认值

                // 0xc0  通信目的对应的相关配置
                0xc0, // PLC 除了标记长度外还有多少个字节
                0x01, // 当前字节以后的字节数
                0x0a,// 2的10次方   1024

                // 0xc1  通信源对应的相关配置  PC
                0xc1, // 上位机配置通讯模式
                0x02, // 当前字节以后的字节数
                0x10, // S7双边模式   也可以选择01- PG    02   OP
                0x00, // 不需要设置机架和插槽

                // 0xc2 S7 连接参数
                0xc2, // PLC - 配置机架号、插槽号
                0x02, // 当前字节以后的字节数
                0x03, // S7单边模式
                0x01  //  如果机架插槽为0，0  0x00   机架*32+插槽   (机架<<5)+插槽
            };
            socket.Send(bytes);
            // 接收COTP CC结果
            socket.Receive(bytes, 0, bytes.Length, SocketFlags.None);
            #endregion

            #region 第三次握手
            // 3、Setup Communication
            bytes = new byte[] {
                // TPKT
                0x03, // 版本号默认3 - 固定值
                0x00, // 保留字节 - 固定0
                0x00,0x19, // 整个请求字节数

                // COTP
                0x02, // 在COTP这个环节，当前字节以后的字节数
                0xf0, // 表示数据传输
                0x80, // 最后一个数据单元 

                // S7 - Header
                0x32, // 协议ID  默认0x32
                0x01, // 向PLC发送一个Job(工作)请求

                0x00,0x00, // 保留字节

                0x00,0x00, // 累加序号
                0x00,0x08, // Parameter 长度
                0x00,0x00, // Data 长度

                // S7 - Parameter
                0xf0, // 功能码 - Setup Communication（设置通信）
                0x00, // 保留字节

                0x00,0x01,  // 任务处理队列长度 - 上位机端-使用默认值就可以
                0x00,0x01,  // 任务处理队列长度 - 上位机端-使用默认值就可以

                0x03,0xc0 //  PDU 长度     960    PLC  反馈   240  480  960

            };
            socket.Send(bytes); // 发送Setup Communication（设置通信）请求

            byte[] resp = new byte[27]; // 接收Setup Communication（设置通信）响应结果
            socket.Receive(resp);// 接收响应结果
            ushort pdu = BitConverter.ToUInt16(new byte[] { resp[26], resp[25] }); // PLC返回的PDU长度
            #endregion
        }

        /// <summary>
        /// 读/写/相关操作 
        /// </summary>
        static void S7Read()
        {
            #region 读取 V/DB块地址的数据请求报文
            // 1、读取单个连续地址
            // 读取 DB1.DBW100 - 10 地址的数据请求报文   连续读取同区域数据
            byte[] bytes = new byte[] {
         // TPKT  - 4bytes
         0x03, // 版本号 默认3 - 固定值
         0x00, // 保留字节 - 固定0
         0x00,0x1f, // 十进制：31

         // COTP
         0x02, // 在COTP这个环节，当前字节以后的字节数
         0xf0,// 数据传输
         0x80, // 最后一个数据单元


         // S7 - Header
         0x32, // 协议ID  默认0x32
         0x01, // 向PLC发送一个Job请求

         0x00,0x00, // 保留字节

         0x00,0x01, // 累加序号
         
         0x00,0x0e, // Parameter 长度
         0x00,0x00, // Data 长度

         // S7 - Parameter 用了设置读写变量标识
         0x04, // 向PLC发送一个读变量的请求 04:读  05:写
         0x01, // Item的数量     Item中包含了请求的地址以及类型相关信息

         // S7 - Parameter - Item
         0x12, // 读变量 功能码
         0x0a,// 当前Item部分，此字节往后还有10个字节
         0x10, // 读数据
         0x02,// 传输数据类型  02：BYTE    04：Word
         0x00,0x14, // 读取的数量

         // DB100   0x00,0x64
         // V-》DB1
         0x00,0x01,  // 对应DB块的编号，如果区域不是DB，这里写0
         // 存储区
         0x84,// DataBlock ->  V       0x81  I区，
         // 变量地址  100 Byte  -2   100 101  占三个字节 
         0x00,0x03,0x20

         // 0000 0000  0000 0011  0010 0000     // 表达字节+位信息
         // DB1.DBX100.5    0x64   0110 0100       0-7   101
         // 0x00 0x03 0x20
     };

            // 2、读取多个不同区域地址
            // 读取 DB1.DBW100 - 10   
            // 读取 DB1.DBW150 - 5
            // 读取 Q0.5-3       位   Bit   01
            // 读取不同区域的数据可不可以？
            // 读取不同类型的数据可不可以？
            // 支持做不同区域的读取，但是这里读取位数据的时候   只能是一个
            bytes = new byte[] {
         // TPKT  - 4bytes
         0x03, // 版本号 默认3 - 固定值
         0x00, // 保留字节 - 固定0
         0x00,0x37,  // 十进制：31+12   43  37

         // COTP
         0x02, // 在COTP这个环节，当前字节以后的字节数
         0xf0,//数据传输
         0x80, // 最后一个数据单元


         // S7 - Header
         0x32, // 协议ID  默认0x32
         0x01, // 向PLC发送一个Job请求

         0x00,0x00, // 保留字节

         0x00,0x01, // 累加序号
         // Parameter 长度
         0x00,0x26,
         // Data 长度
         0x00,0x00,

         #region 设置参数项个数
         // S7 - Parameter
         0x04, // 向PLC发送一个读变量的请求
         0x03, // Item的数量     Item中包含了请求的地址以及类型相关信息
         #endregion

         #region S7 - Parameter - Item  - 1
         // S7 - Parameter - Item  - 1
         0x12, // 读变量 功能码
         0x0a,// 当前Item部分，此字节往后还有10个字节
         0x10, // 读数据
         0x02,// 传输数据类型  02：BYTE    04：Word
         0x00,0x14, // 读取的数量

         // DB100   0x00,0x64
         // V-》DB1
         0x00,0x01,  // 对应DB块的编号，如果区域不是DB，这里写0
         // 存储区
         0x84,// DataBlock ->  V       0x81  I区，
         // 变量地址  100 Byte  -2   100 101  占三个字节 
         0x00,0x03,0x20,
         #endregion


         #region S7 - Parameter - Item  -2
         // S7 - Parameter - Item  -2
         0x12, // 读变量 功能码
         0x0a,// 当前Item部分，此字节往后还有10个字节
         0x10, // 读数据
         0x04,// 传输数据类型  02：BYTE    04：Word
         0x00,0x05, // 读取的数量

         #region DB150   读取起始DB块地址为150
         0x00,0x01, // 对应DB块的编号，如果区域不是DB，这里写0
         // 存储区
         0x84,
         // 变量地址  150 Byte
         0x00,0x04,0xB0,
         #endregion
	 
	 // S7 - Parameter - Item  -3
                // 如果请求的位数据在中间Item而不在最的话，那么这个数据返回的时候会多一个Fill Bytes字节
                0x12,
                0x0a,// 当前Item部分，此字节往后还有10个字节
                0x10,
                0x01,// 传输数据类型  01:BIT   02：BYTE    04：Word
                0x00,0x01, // 读取的数量

                0x00,0x00,  
                // 存储区
                0x82,
                // 变量地址  0.5 Bit 
                // 0000 0000   0000 0000    0000 0101
                0x00,0x00,0x06,
         #endregion

     };
            #endregion


            #region 读取Q区地址数据请求报文
            // 3、单独请求位地址
            // 单独请求Q0.5地址数据
            // 注意：就算是单个位区域数据请求，也得是一个状态一个状态的获取，但是可以按照字节获取
            bytes = new byte[] {
         // TPKT  - 4bytes
         0x03, // 版本号 默认3 - 固定值
         0x00, // 保留字节 - 固定0
         0x00,0x37,  // 十进制：31+12   43  37  55-24=31   1F

         // COTP
         0x02, // 在COTP这个环节，当前字节以后的字节数
         0xf0,//数据传输
         0x80, // 最后一个数据单元


         // S7 - Header
         0x32, // 协议ID  默认0x32
         0x01, // 向PLC发送一个Job请求

         0x00,0x00, // 保留字节

         0x00,0x01, // 累加序号
         // Parameter 长度
         0x00,0x26,
         // Data 长度
         0x00,0x00,

         // S7 - Parameter
         0x04, // 向PLC发送一个读变量的请求
         0x03, // Item的数量     Item中包含了请求的地址以及类型相关信息



         // S7 - Parameter - Item 
         // 如果请求的位数据在中间Item而不在最的话，那么这个数据返回的时候会多一个Fill Bytes字节

         // Q0.5
         0x12, // 读变量 功能码
         0x0a,// 当前Item部分，此字节往后还有10个字节
         0x10, // 语法
         0x01,// 传输数据类型  01:BIT   02：BYTE    04：Word
         0x00,0x01, // 读取的数量

         0x00,0x00, // 对应DB块的编号，如果区域不是DB，这里写0
         // 存储区
         0x82,  // Q区是 82
         // 变量地址  0.5 Bit 
         // 0000 0000   0000 0000    0000 0101
         0x00,0x00,0x05,


         // Q0.6
         0x12, // 读变量 功能码
         0x0a,// 当前Item部分，此字节往后还有10个字节
         0x10,
         0x01,// 传输数据类型  01:BIT   02：BYTE    04：Word
         0x00,0x01, // 读取的数量

         0x00,0x00,  
         // 存储区
         0x82,
         // 变量地址  0.5 Bit 
         // 0000 0000   0000 0000    0000 0101
         0x00,0x00,0x06,

         //Q0.7
         0x12, // 读变量 功能码
         0x0a,// 当前Item部分，此字节往后还有10个字节
         0x10,
         0x01,// 传输数据类型  01:BIT   02：BYTE    04：Word
         0x00,0x01, // 读取的数量

         0x00,0x00,  
         // 存储区
         0x82,
         // 变量地址  0.5 Bit 
         // 0000 0000   0000 0000    0000 0101
         0x00,0x00,0x07
     };
            #endregion


            // 4、请求字符串数据
            // "Hello" -> 0x48 0x65 0x6C 0x6C 0x6F
            bytes = new byte[] {
         // TPKT  - 4bytes
         0x03, // 版本号 默认3 - 固定值
         0x00, // 保留字节 - 固定0
         0x00,0x1F,  // 十进制：31

         // COTP
         0x02, // 在COTP这个环节，当前字节以后的字节数
         0xf0,//数据传输
         0x80, // 最后一个数据单元


         // S7 - Header
         0x32, // 协议ID  默认0x32
         0x01, // 向PLC发送一个Job请求

         0x00,0x00, // 保留字节

         0x00,0x01, // 累加序号
         // Parameter 长度
         0x00,0x0E,
         // Data 长度
         0x00,0x00,

         // S7 - Parameter
         0x04, // 向PLC发送一个读变量的请求
         0x01, // Item的数量     Item中包含了请求的地址以及类型相关信息



         // S7 - Parameter - Item 
         // 如果请求的位数据在中间Item而不在最的话，那么这个数据返回的时候会多一个Fill Bytes字节

         0x12, // 读变量 功能码
         0x0a,// 当前Item部分，此字节往后还有10个字节
         0x10, // 读数据
         0x02,// 传输数据类型  01:BIT   02：BYTE    04：Word
         0x00,0x0C, // 读取的数量

         0x00,0x01, // 对应DB块的编号，如果区域不是DB，这里写0
         // 存储区
         0x84,
         // 变量地址  0.5 Bit 
         // 0000 0000   0000 0000    0101 0000
         0x00,0x00,0x00
     };

            socket.Send(bytes);// 读取地址变量数据的请求
        }

        /// <summary>
        /// 存储区、地址、类型、数据
        /// </summary>
        static void S7Write()
        {
            // 1、修改任务类型  04读   05写
            // 2、相关长度Item个数    DBW20
            //           Data长度
            //           整个报文长度
            byte[] bytes = new byte[] {
                // TPKT  - 4bytes
                0x03, // 版本号 默认3 - 固定值
                0x00, // 保留字节 - 固定0
                0x00,0x38,  // 十进制：39+17=56   48+8  **************************

                // COTP
                0x02, // 在COTP这个环节，当前字节以后的字节数
                0xf0, //数据传输
                0x80,   // 最后一个数据单元


                // S7 - Header
                0x32,// 协议ID  默认0x32
                0x01,// 向PLC发送一个Job请求
                0x00,0x00,// 保留字节
                0x00,0x01, // 累加序号
                
                // Parameter 长度***************
                0x00,0x1A,
                // Data 长度********************
                0x00,0x0D,

                // S7 - Parameter - 设置读写变量
                0x05, // 向PLC发送一个写变量的请求   04:读  05:写
                0x02, // Item的数量     **********************************

                // 设置写入字节参数配置
                // S7 - Parameter - Item-1   DBW20 - 22
                0x12,// 读变量 功能码
                0x0a,// 当前Item部分，此字节往后还有10个字节
                0x10,// 读数据
                0x04,// 传输数据类型  02：BYTE    04：Word
                0x00,0x02, // 写入的数量

                0x00,0x01,  // 对应DB块的编号，如果区域不是DB，这里写0
                // 存储区
                0x84,// 84 - DB区
                0x00,0x00,0xA0,  // 20.0

                // 0000 0000  0000 0000  1010 0000

                // 设置写入位参数配置
                // S7 - Parameter - Item-2   Q0.5
                0x12,// 读变量 功能码
                0x0a,// 当前Item部分，此字节往后还有10个字节
                0x10,// 读数据
                0x01,// 传输数据类型  01:Bit  02：BYTE    04：Word
                0x00,0x01, // 写入的数量

                0x00,0x00,  // 对应DB块的编号，如果区域不是DB，这里写0
                // 存储区
                0x82, // Q区是 82
                0x00,0x00,0x05, // 地址 Q0.5

                // 写入字节参数数据
                // S7 - Data - Item-1
                0x00,// 保留字节
                0x04,// 数据类型
                0x00, 0x20,    // 长度按照位长度来计算     16
                0x02,0x9C,0x02,0x9D,// 写入的数据  700 701

                // 写入位参数数据
                // S7 - Data - Item-2
                0x00,// 保留字节
                0x03,   // 数据类型
                0x00, 0x01,    // 长度   字节数*8
                0x00 // 写入的数据  0/1

                // 这里是否可以写多个状态？
                 // ***这里不能做多个位的写入，会报0x03的异常***
            };

            socket.Send(bytes);
        }

        /// <summary>
        /// 控制PLC启动运行 - 命令报文
        /// 1200/1500系列PLC不支持此命令
        /// </summary>
        static void S7Run()
        {
            byte[] bytes = new byte[] {
                // TPKT
                0x03, // 版本号默认3 - 固定值
                0x00, // 保留字节 - 固定0
                0x00,0x25, // 整个请求字节数

                // COTP
                0x02, // 在COTP这个环节，当前字节以后的字节数
                0xf0, // 表示数据传输
                0x80, // 最后一个数据单元

                // S7-Header
                0x32, // 协议ID  默认0x32
                0x01, // 向PLC发送一个Job请求
                0x00,0x00,0x00,0x00, // 保留字节

                // Parameter Length - 参数长度
                0x00,0x14,  // 10#20
                // Data Length - 数据长度
                0x00,0x00,

                // S7-Parameter
                0x28,// 控制PLC启动
                0x00,0x00,0x00,0x00,0x00,0x00,0xfd, // 运行命令 - 相对停止多了这4个字节

                0x00,0x00, // 保留字节
                0x09, // 从这个字节往后有9个字节
                //字符串 -表示 "P_PROGRAM" 命令
                0x50,0x5F,0x50,0x52,0x4f,0x47,0x52,0x41,0x4d
            };
            //string str = "P_PROGRAM";
            //byte[] str_bytes = Encoding.ASCII.GetBytes(str);
            socket.Send(bytes);


        }

        /// <summary>
        /// 控制PLC启动运行 - 命令报文
        /// 1200/1500系列PLC不支持此命令
        /// </summary>
        static void S7Stop()
        {
            byte[] bytes = new byte[] {
                // TPKT
                0x03, // 版本号默认3 - 固定值
                0x00, // 保留字节 - 固定0
                0x00,0x21, // 整个请求字节数

                // COTP
                0x02, // 在COTP这个环节，当前字节以后的字节数
                0xf0, // 表示数据传输
                0x80, // 最后一个数据单元

                // S7-Header
                0x32, // 协议ID  默认0x32
                0x01, // 向PLC发送一个Job请求
                0x00,0x00,0x00,0x00, // 保留字节

                // Parameter Length
                0x00,0x10,  // 10#20
                // Data Length
                0x00,0x00,

                // S7-Parameter
                0x29,// 控制PLC停止
                0x00,0x00,0x00,0x00,0x00, // 停止命令
                
                //0x00,0xfd,   相对启动来讲，多了这4个字节
                //0x00,0x00,

                0x09, // 从这个字节往后有9个字节
                // P_PROGRAM
                0x50,0x5F,0x50,0x52,0x4f,0x47,0x52,0x41,0x4d // 字符串 -表示 "P_PROGRAM" 命令
            };

            socket.Send(bytes);
        }

        /// <summary>
        /// 读取PLC系统时间命令报文
        /// </summary>
        static void S7GetDateTime()
        {
            byte[] bytes = new byte[] {
                // TPKT
                0x03, // 版本号默认3 - 固定值
                0x00, // 保留字节 - 固定0
                0x00,0x1d, // 整个请求字节数

                // COTP
                0x02, // 在COTP这个环节，当前字节以后的字节数
                0xf0, // 表示数据传输
                0x80, // 最后一个数据单元

                // S7-Header
                0x32, // 协议ID  默认0x32
                0x07,//UserData - 读取用户数据
                0x00,0x00,0x00,0x00, // 保留字节

                // Parameter Length
                0x00,0x8,  // 10#20
                // Data Length
                0x00,0x04,

                // S7-Parameter
                0x00,0x01,0x12,
                0x04,// Parameter部分后续还有4个字节的内容
                0x11, // 请求一个Data信息
                // 0100 0111
                0x47, // 表示请求-时间日期信息
                0x01,// Read Clock - 读取时钟
                0x00, // 序列号

                // S7-Data
                0x0a, // 后续有10个长度的字节
                0x00,
                0x00,0x00
            };
            socket.Send(bytes);
        }

        /// <summary>
        /// 设置PLC系统时间命令报文
        /// </summary>
        static void S7SetDateTime()
        {
            // 2025-10-01 20:05:00 4
            byte[] bytes = new byte[] {
                // TPKT
                0x03, // 版本号默认3 - 固定值
                0x00, // 保留字节 - 固定0
                0x00,0x27, // 整个请求字节数

                // COTP
                0x02, // 在COTP这个环节，当前字节以后的字节数
                0xf0, // 表示数据传输
                0x80, // 最后一个数据单元

                // S7-Header
                0x32, // 协议ID  默认0x32
                0x07,// UserData - 读取用户数据
                0x00,0x00,0x00,0x00, // 写入

                // Parameter Length
                0x00,0x08, 
                // Data Length
                0x00,0x0E, // 14


                // S7-Paramter
                0x00,0x01,0x12, // 功能码 - 参数类型 - 读写变量
                0x04, // Parameter部分后续还有4个字节的内容
                0x11, // 请求一个Data信息
                0x47, // 表示请求-时间日期信息

                0x02,// Set Clock - 设置时钟
                0x00, // 序列号

                // S7-Data
                0xff, // 设定时间
                0x09, // 字符串类型
                0x00,0x0a,// 后续有10个长度的字节
                0x00, // 保留字节
                0x19, // 没什么作用
                // Year:25
                0x25,
                // Month:10
                0x10,
                // Day:01
                0x01,
                // Hour:20
                0x20,
                // Minute:05
                0x05,
                // Second:00
                0x00,
                // 毫秒:0  和星期
                0x00,0x04
                // 0000 0000   0000 0  100
            };
            socket.Send(bytes);

            //DateTime.Now.Year   // 10#2024
            // "2024"  - "24"  ->  0x24
            // 24 - 0x18
        }
        #endregion

        
    }
}
